﻿/**
Library: transition managing library (TraM)
Author: Valentyn Derkach 2007.07.04
———————————————————————————————————————————————————————————————————————————————————————————————————————————————————
2008.04.23
	Added isExecuting and isRunning getters.
	Replaced 'transition' property with the 'name'.
	Added setProcessor, setSubject and setProperties methods.
	Changed processor, subject and properties setters - switched to read and write.
	Removed reStart method.
	Renamed restartProcessor method and arguments to rebootProcessor.
	Renamed completeTransitionInternal into doCompleteTransition.
	Renamed completeTransition into stop.
	Added canStart method.
	Changed start method:
		- added subject, properties and processor parameters
		- added isStart call
		- iterationFunction has been moved outside and became a private method doIteration
	
	TODO:
		- implement processor locking during the transition's execution.
		- check the necessity of the completeEvent, executeEvent, interrupeEvent variables
2008.10.22
	Switched from the itemsList queue to an array
/**/
package com.bizmlib.tram
{
	import flash.display.DisplayObject;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.utils.*;
	
	import com.bizmlib.managers.UIRenderManager;
	
	
	[Event(name="completed", type="com.bizmlib.tram.TraMEvent")]
	[Event(name="execution", type="com.bizmlib.tram.TraMEvent")]
	[Event(name="interrupt", type="com.bizmlib.tram.TraMEvent")]
	
	
	public class TraM extends EventDispatcher
	{
		public static const STATE_STILL:Number = 0;
		public static const STATE_RUNNING:Number = 1;
		
		
		//private static var itemsList:TraM = null;
		private static var itemsList:Array = new Array();
		private static var __commonFPS:Number = 40;
		private static var defaultIntervalValue:Number = 20;
		/**
			tintObjects
			│
			├item
			│	├threadID
			│	├transition
			│	├targetObject
			│	├properties
			│	├next
			│	└prev
			├item
			│	└...
			└...
		/**/
		
		
		private var __fps:Number;
		private var threadID:Number = -1;
		private var __name:String = "unknown";
		private var __subject:Object;
		private var __properties:Object;
		private var __engine:IEngine;
		private var __updateOnEnterFrame:Boolean;
		private var completeEvent:Boolean = true;
		private var executeEvent:Boolean = true;
		private var interruptEvent:Boolean = true;
		protected var renderManager:UIRenderManager;
		/**
		private var prev:TraM;
		private var next:TraM;
		/**/
		private var iterationFunction:Function;
		private var stateRun:Number;


		// ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————
		// CONSTRUCTOR. does nothing
		public function TraM(subject:Object = undefined, properties:Object = undefined, engine:IEngine = undefined)
		{
			super();
			
			stateRun = STATE_STILL;

			//if(itemsList==undefined){itemsList=null;}

			init(subject, properties, engine);
		}
		// ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————


		// ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————
		// PROPERTIES
		public static function set commonFPS(value:Number):void{
			__commonFPS = Math.abs(value) || __commonFPS;
		}
		public static function get commonFPS():Number{return __commonFPS;}
		public function set fps(value:Number):void{
			__fps=Math.abs(value) || null;
		}
		public function get fps():Number{return __fps;}
		public function get realFPS():Number
		{
			return fps || commonFPS;
		}
		/**
		 * The container of properties names and their final values, which need to be reached by the end of the transition's execution.
		 * @return	Object
		 */
		public function set properties(value:Object):void
		{
			setProperties(value);
		}
		public function get properties():Object
		{
			return __properties;
		}
		/**
		 * The object to which the transition applies.
		 * @return	Object
		 */
		public function set subject(value:Object):void
		{
			setSubject(value);
		}
		public function get subject():Object
		{
			return __subject;
		}
		/**
		 * The engine which does all calculations.
		 * @return	ITweenProcessor
		 */
		public function set engine(value:IEngine):void
		{
			setEngine(value);
		}
		public function get engine():IEngine
		{
			return __engine;
		}
		
		/**
		 * Returns the name of the transition, or in other words the type of the transition.
		 * @return	String
		 */
		public function get name():String
		{
			return __name;
		}
		
		/**
		 * Identifies whether the transition is running or not. Read-only. The same as isRunning property.
		 * @return	Boolean
		 */
		public function get isExecuting():Boolean
		{
			//return threadID==-1?false:true;
			return isRunning;
		}
		/**
		 * Identifies whether the transition is running or not. Read-only. The same as isExecuting property.
		 * @return	Boolean
		 */
		public function get isRunning():Boolean
		{
			return stateRun==STATE_RUNNING;
		}
		
		public function set updateOnEnterFrame(value:Boolean):void
		{
			__updateOnEnterFrame = value;
			checkUIRenderManager();
		}
		public function get updateOnEnterFrame():Boolean
		{
			return __updateOnEnterFrame;
		}
		// ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————
		
		
		
		// ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————
		/**
		 * Changes the subject, properties, processor properties and also reboots the processor. You don't necesserily have to use this method. Consider about
		 * using the setters for the above properties and rebootProcessor method instead. Also you can set those values directly in the start call.
		 */
		final public function reInit(subject:Object = undefined, properties:Object = undefined, engine:IEngine = undefined, restartEngine:Boolean = false):void
		{
			//trace(this + ".reInit(" + arguments + ")");
			
			init(subject, properties, engine);
			if(restartEngine)
			{
				this.restartEngine();
			}
		}
		
		/**
		 * Resets the dynamic values of the processor.
		 */
		final public function restartEngine():void
		{
			engine.reStart();
		}
		
		/**
		 * Starts the transition's execution. 
		 */
		final public function start(subject:Object = undefined, properties:Object = undefined, engine:IEngine = undefined, restartEngine:Boolean = false):void
		{
			//trace(this + ".start(" + arguments + ")");
			
			if(!canStart(true))
			{
				return;
			}
			
			if(isRunning)
			{
				if(compareProperties(properties))
				{
					return;
				}
			}
			
			if(threadID<0)
			{
				//closeTransition(subject, transition);
				addTransitionInTheList(this);
			}
			else
			{
				clearInterval(threadID);
				threadID = -1;
				
				stateRun = STATE_STILL;
			}
			
			init(subject, properties, engine);
			if(restartEngine)
			{
				this.restartEngine();
			}
			
			onStart();
			
			threadID = setInterval(doIteration, Math.round(1000/realFPS));
			stateRun = STATE_RUNNING;
			doIteration();
		}
		
		/**
		 * Stops the transition's execution.
		 */
		final public function stop():void
		{
			doCompleteTransition(false);
		}
		
		public function clean():void
		{
			stop();
			
			__subject = null;
			__properties = null;
			__engine = null;
			
			removeRenderManager();
		}
		// ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————

		
		
		// ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————
		final protected function setName(newName:String = undefined):void
		{
			__name = newName || name;
		}
		private function init(subject:Object = undefined, properties:Object = undefined, engine:IEngine = undefined):void
		{
			//trace(this + ".init(" + arguments + ")");
			
			setSubject(subject);
			setProperties(properties);
			setEngine(engine);
		}
		protected function setSubject(subject:Object = undefined):void
		{
			if(!subject)
			{
				return;
			}
			
			if(isRunning)
			{
				throw new Error(this + " can't set subject while the transition is running");
				return;
			}
			
			__subject = subject;
			
			checkUIRenderManager();
		}
		protected function compareProperties(comparingProperties:Object):Boolean
		{
			for(var propertyName:String in comparingProperties)
			{
				if(!properties.hasOwnProperty(propertyName) || properties[propertyName]!=comparingProperties[propertyName])
				{
					return false;
				}
			}
			
			return true;
		}
		protected function setProperties(properties:Object = undefined):Boolean
		{
			if(!properties && !((properties is Number) && !isNaN(properties as Number)))
			{
				return false;
			}
			
			if(isRunning)
			{
				throw new Error(this + " can't set properties while the transition is running");
				return false;
			}
			
			__properties = properties;
			
			return true;
		}
		private function setEngine(engine:IEngine = undefined):void
		{
			if(!engine)
			{
				return;
			}
			
			if(isRunning)
			{
				throw new Error(this + " can't set engine while the transition is running");
				return;
			}
						
			__engine = engine;
		}
		/**
		 * Adds the TraM instance to the list of currently running TraM instances.
		 *	@param	controller	a TraM instance which should be added to a list
		 */
		private static function addTransitionInTheList(controller:TraM):void
		{
			/**
			controller.next = itemsList;
			controller.prev = null;
			if(itemsList)
			{
				itemsList.prev = controller;
			}
			itemsList = controller;
			/**/
			if(!isTransitionInTheList(controller))
			{
				itemsList.push(controller);
			}
		}
		/**
		 * Checks if the TraM instance is in the list of the currently running TraM instances.
		 *	@param	controller	a TraM instance
		 *	@return				boolean value
		 */
		private static function isTransitionInTheList(controller:TraM):Boolean
		{
			for each(var tramInstance:TraM in itemsList)
			{
				if(tramInstance == controller)
				{
					return true;
				}
			}
			
			return false;
		}
		private function broadcastEvent(eventName:String):void
		{
			dispatchEvent(new TraMEvent(eventName, subject, properties, name));
		}
		
		/**
		 * Performs an iteration action. Sometimes TraM instance is destroyed immediatelly after the COMPLETED event is dispatched. In that case a line engine.onAfterIteration()
		 * leads to an exception thrown by a flash player case engine is already set to null at that point. That's why i call engine's onAfterIteration method two times, and ther's
		 * an if statement for the line which follows a doCompleteTransition call.
		 */
		private function doIteration():void
		{
			//trace(this + ".doIteration(" + arguments + ")");
			
			engine.onBeforeIteration();
			transitionIteration();
			broadcastEvent(TraMEvent.EXECUTION);
			
			if(engine.isFinal())
			{
				if(engine)
				{
					engine.onAfterIteration();
				}
				doCompleteTransition(true);
			}
			else
			{
				engine.onAfterIteration();
			}
		}
		/**
		 * Finds whether it's possible to start the transition with the current configuration or not.
		 */
		private function canStart(throwError:Boolean = false):Boolean
		{
			/**
			var currentTransition:TraM = itemsList as TraM;
			while(currentTransition)
			/**/
			for each(var currentTransition:TraM in itemsList)
			{
				if(currentTransition!=this && currentTransition.engine==engine)
				{
					if(throwError)
					{
						throw new Error(this + " can't start - the same engine is already in use by another transition.");
					}
					return false;
				}
				//currentTransition = currentTransition.next;
			}
			return true;
		}
		protected function onStart():void
		{
			// this is an empty method that is called from within the start method
		}
		
		protected function checkUIRenderManager():void
		{
			removeRenderManager();
			
			if((subject is DisplayObject) && updateOnEnterFrame)
			{
				renderManager = new UIRenderManager(subject as DisplayObject);
				renderManager.addEventListener(Event.RENDER, doRender);
			}
		}
		protected function removeRenderManager():void
		{
			if(renderManager)
			{
				renderManager.removeEventListener(Event.RENDER, doRender);
				renderManager.clean();
				renderManager = null;
			}
		}
		protected function doRender(event:Event):void
		{
		}
		// ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————



		// ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————
		// TRANSITION REMOVAL. finds the transition controller(s) using movieClip and transitionName and removes it
		/**
		public static function closeTransition(subject:Object, transitionName:String):void
		{
			return;
			var curController:Object = itemsList;
			while(curController!=null){
				if(curController.subject==subject && curController.transition==transitionName){
					var nextController:Object = curController.next;
					curController.completeTransition();
					curController = nextController;
				}else{
					curController = curController.next;
				}
			}
		}
		/**/
		// ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————






		// ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————
		// TRANSITION COMPLETION. every transition method should call this after the transition is finished. this method
		// removes the transition controller object from the list and stops the execution of the transition thread
		private function doCompleteTransition(broadcastCompleteEvent:Boolean):void
		{	
			clearInterval(threadID);
			threadID = -1;
			
			if(updateOnEnterFrame)
			{
				doRender(new Event(Event.RENDER));
			}
			
			stateRun = STATE_STILL;
			/**
			if(next)
			{
				next.prev = prev;
			}
			if(prev!=null)
			{
				prev.next = next;
			}
			else
			{
				itemsList = next;
			}
			/**/
			for(var i:Number=0; i<itemsList.length; i++)
			{
				if(itemsList[i]==this)
				{
					itemsList.splice(i, 1);
				}
			}
			/**/
			if(completeEvent && broadcastCompleteEvent)
			{
				broadcastEvent(TraMEvent.COMPLETED);
			}
		}
		// ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————
		override public function toString():String
		{
			return "[TraM	name=" + name + "	subject=" + subject + "]";
		}


		// ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————
		// TRANSITION ITERATION. is an empty transition iteration function. override it in your own transitioning class
		protected function transitionIteration():void
		{
			//trace(this + ".transitionIteration()");
			if(executeEvent){broadcastEvent(TraMEvent.TYPE_EXECUTION);}
		}
		// ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————

	}
}